package demo.javax.sql;

import java.sql.*;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariConfigMXBean;
import com.zaxxer.hikari.HikariDataSource;

import com.zaxxer.hikari.HikariPoolMXBean;
import demo.db.mysql.MySqlDemo;
import demo.java.lang.ThreadDemo;
import demo.java.sql.ConnectionDemo;
import demo.java.sql.JdbcDemo;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * HikariCP 是一个高效的数据库连接池。 HikariCP号称是现在性能最好的JDBC连接池组件。 Hikari来自日文，是“光”，阳光的光。
 * <p>
 * 官网详细地说明了HikariCP所做的一些优化，总结如下：
 * <li>字节码精简：优化代码，直到编译后的字节码最少，这样，CPU缓存可以加载更多的程序代码；
 * <li>优化代理和拦截器：减少代码，例如HikariCP的Statement proxy只有100行代码，只有BoneCP的十分之一；
 * <li>自定义数组类型（FastStatementList）代替ArrayList：避免每次get()调用都要进行range check，避免调用remove()时的从头到尾的扫描；
 * <li>自定义集合类型（ConcurrentBag）：提高并发读写的效率；
 * <li>其他针对BoneCP缺陷的优化，比如对于耗时超过一个CPU时间片的方法调用的研究（但没说具体怎么优化）。
 *
 * <p>
 * 关于可靠性方面，也是有实验和数据支持的。对于数据库连接中断的情况，通过测试getConnection()，各种CP的不相同处理方法如下： （所有CP都配置了跟connectionTimeout类似的参数为5秒钟）
 * <li>HikariCP：等待5秒钟后，如果连接还是没有恢复，则抛出一个SQLExceptions 异常；后续的getConnection()也是一样处理；
 * <li>C3P0：完全没有反应，没有提示，也不会在“CheckoutTimeout”配置的时长超时后有任何通知给调用者；然后等待2分钟后终于醒来了，返回一个error；
 * <li>Tomcat：返回一个connection，然后……调用者如果利用这个无效的connection执行SQL语句……结果可想而知；大约55秒之后终于醒来了，这时候的getConnection()终于可以返回一个error，但没有等待参数配置的5秒钟，而是立即返回error；
 * <li>BoneCP：跟Tomcat的处理方法一样；也是大约55秒之后才醒来，有了正常的反应，并且终于会等待5秒钟之后返回error了；
 *
 */
public class HikariDataSourceDemo {

    private static final Logger logger = LoggerFactory.getLogger(HikariDataSourceDemo.class);

    @Test
    public void testHikariDataSource() throws SQLException {
        String jdbcUrl = "jdbc:mysql://localhost:3306/ops_alert?useSSL=false&serverTimezone=UTC";
        String username = "root";
        String password = "rootroot";

        HikariDataSource ds = init(jdbcUrl, username, password);
        try (Connection conn = ds.getConnection()) {
            // 执行SQL语句
            Statement statement = conn.createStatement();
            ResultSet resultSet = statement.executeQuery("SELECT * FROM alert_rule");
            JdbcDemo.printResultSet(resultSet);
        } catch (Exception e) {
            e.printStackTrace();
        }

        ds.close();
    }

    @Test
    public void testChangeJdbcUrl() throws SQLException {
        String jdbcUrl = "jdbc:mysql://localhost:3306/ops_alert?useSSL=false&serverTimezone=UTC";
        String username = "root";
        String password = "rootroot";

        HikariDataSource ds = init(jdbcUrl, username, password);
        int i = 0;
        while (i < 100) {
            try (Connection conn = ds.getConnection()) {
                // 执行SQL语句
                Statement statement = conn.createStatement();
                ResultSet resultSet = statement.executeQuery("SELECT * FROM alert_rule");
                JdbcDemo.printResultSet(resultSet);
                printPoolStats(ds);
            } catch (Exception e) {
                e.printStackTrace();
            }
            i++;
            if (i == 50) {
                System.err.println("------------------ change jdbcUrl to " + jdbcUrl);
                jdbcUrl = "jdbc:mysql://cex-mysql-ex-test-cluster.cluster-c5mgk4qm8m2z.ap-southeast-1.rds.amazonaws.com:3358/ops_alert";
                username = "ops_arthas_rw";
                password = "VYUmItYvMaQ2Tzdn";
                try {
                    ds.setJdbcUrl(jdbcUrl);
                    ds.setUsername(username);
                    ds.setPassword(password);
                } catch (IllegalStateException e) {
                    System.err.println("change jdbcUrl failed, IllegalStateException: " + e.getMessage());
                }
                ds.setMaximumPoolSize(50);
            }
            ThreadDemo.safeSleep(500);
        }

    }

    /**
     * 测试1、DS关闭后，已获取的连接是否可用。
     * 结论：
     * 1. 已获取的连接，在DS关闭后，是不可用的。
     * 2. 已获取的连接，在DS关闭后，会抛出SQLNonTransientConnectionException异常。
     *
     * @throws SQLException
     */
    @Test
    public void testCloseDataSource1() throws SQLException {
        String jdbcUrl = "jdbc:mysql://localhost:3306/ops_alert?useSSL=false&serverTimezone=UTC";
        String username = "root";
        String password = "rootroot";

        HikariDataSource ds = init(jdbcUrl, username, password);
        // 打印连接池状态
        startPrintPoolStatsAsync(ds);
        System.out.println("====================1. 正常获取连接，执行查询语句");
        // 执行SQL语句
        try (Connection conn = ds.getConnection();
             Statement statement = conn.createStatement()) {
            // 执行SQL语句
            ResultSet resultSet = statement.executeQuery("SELECT * FROM alert_rule");
            JdbcDemo.printResultSet(resultSet);
            printPoolStats(ds);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("===2. 先获取连接,再关闭连接池，执行查询语句");
        // 获取连接
        Connection conn = null;
        Statement statement = null;
        try {
            conn = ds.getConnection();
            statement = conn.createStatement();
            // 异步关闭连接池：关闭连接池，会断开所有空闲连接，并终止或关闭所有活动连接。
            ds.close();
            System.out.println("====关闭连接池，会断开所有空闲连接，并终止或关闭所有活动连接");
            ResultSet resultSet = statement.executeQuery("SELECT * FROM alert_rule");
            JdbcDemo.printResultSet(resultSet);
        } catch (SQLNonTransientConnectionException e) {
            e.printStackTrace();
        } finally {
            try {
                statement.close();
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }


        ThreadDemo.safeSleep(1000);

    }

    /**
     * 测试2、验证DS关闭后，能否再次从连接池获取连接。
     * 结论：
     * 1. DS关闭后，再次从连接池获取连接，会抛出SQLException异常：HikariDataSource has been closed.。
     *
     * @throws SQLException
     */
    @Test
    public void testCloseDataSource2() throws SQLException {
        String jdbcUrl = "jdbc:mysql://localhost:3306/ops_alert?useSSL=false&serverTimezone=UTC";
        String username = "root";
        String password = "rootroot";

        HikariDataSource ds = init(jdbcUrl, username, password);
        // 打印连接池状态
        startPrintPoolStatsAsync(ds);
        System.out.println("====================1. 正常获取连接，执行查询语句");
        // 执行SQL语句
        try (Connection conn = ds.getConnection();
             Statement statement = conn.createStatement();) {
            // 执行SQL语句
            ResultSet resultSet = statement.executeQuery("SELECT * FROM alert_rule");
            JdbcDemo.printResultSet(resultSet);
            printPoolStats(ds);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 关闭连接池
        closeDsAsync(ds);
        ThreadDemo.safeSleep(500);

        System.out.println("===  再次从连接池获取连接，执行查询语句");
        try {
            Connection conn = ds.getConnection();
            ConnectionDemo.testConnection(conn);
        } catch (Exception e) {
            e.printStackTrace();
        }


        ThreadDemo.safeSleep(1000);

    }

    /**
     * 初始化连接池
     *
     */
    public static HikariDataSource init(String jdbcUrl, String username, String password) {
        // 1. 创建 HikariConfig 并配置
        HikariConfig config = new HikariConfig();
        config.setPoolName("hikari-ds");
        config.setDriverClassName(MySqlDemo.JDBC_DRIVER_V8);
        config.setJdbcUrl(jdbcUrl);
        config.setUsername(username);
        config.setPassword(password);

        // 2. 设置连接池参数
        // 最大连接数
        config.setMaximumPoolSize(10);
        // 最小空闲连接数
        config.setMinimumIdle(5);
        // 连接超时时间（毫秒）
        config.setConnectionTimeout(30000);
        // 空闲连接超时时间（毫秒）
        config.setIdleTimeout(600_000);
        // 连接最大生命周期（毫秒）
        config.setMaxLifetime(1800_000);


        config.setConnectionTestQuery("SELECT 1");
        config.setAutoCommit(true);

        return new HikariDataSource(config);
    }

    public static void startPrintPoolStatsAsync(HikariDataSource dataSource) {
        Thread t = new Thread(
                () -> {
                    while (true) {
                        printPoolStats(dataSource);
                        ThreadDemo.safeSleep(200);
                    }
                });
        t.setDaemon(true);
        t.start();
    }

    /**
     * 关闭连接池，会断开所有空闲连接，并终止或关闭所有活动连接。
     *
     * @param dataSource
     */
    public static void closeDsAsync(HikariDataSource dataSource) {
        Thread t = new Thread(
                () -> {
                    dataSource.close();
                    System.out.println("====关闭连接池，会断开所有空闲连接，并终止或关闭所有活动连接");
                    printPoolStats(dataSource);
                });
        t.start();
    }

    public static void printPoolStats(HikariDataSource dataSource) {
        HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
        String poolName = dataSource.getPoolName();

        int totalConnections = poolMXBean.getTotalConnections();
        int activeConnections = poolMXBean.getActiveConnections();
        int idleConnections = poolMXBean.getIdleConnections();
        int threadsAwaitingConnection = poolMXBean.getThreadsAwaitingConnection();
        System.out.println("=== HikariCP 连接池状态 ===");
        logger.info("{} stats (total={}, active={}, idle={}, waiting={})",
                poolName, totalConnections, activeConnections, idleConnections, threadsAwaitingConnection);
        System.out.println(String.format("DS：%s totalConnections: %d, activeConnections: %d, idleConnections: %d, 等待获取连接的线程数: %d",
                poolName, totalConnections, activeConnections, idleConnections, threadsAwaitingConnection));

        HikariConfigMXBean configMXBean = dataSource.getHikariConfigMXBean();
        logger.info("{} config (max={}, min={}, timeout={}ms)",
                poolName, configMXBean.getMaximumPoolSize(), configMXBean.getMinimumIdle(), configMXBean.getConnectionTimeout());
        System.out.println(String.format("DS：%s max=%s, min=%s, timeout=%dms",
                poolName, configMXBean.getMaximumPoolSize(), configMXBean.getMinimumIdle(), configMXBean.getConnectionTimeout()));
    }

}
